package rescuecore2.standard.kernel.comms;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import kernel.AbstractCommunicationModel;
import rescuecore2.config.Config;
import rescuecore2.log.Logger;
import rescuecore2.messages.Command;
import rescuecore2.standard.entities.AmbulanceCentre;
import rescuecore2.standard.entities.AmbulanceTeam;
import rescuecore2.standard.entities.BirdMan;
import rescuecore2.standard.entities.Civilian;
import rescuecore2.standard.entities.FireBrigade;
import rescuecore2.standard.entities.FireStation;
import rescuecore2.standard.entities.PoliceForce;
import rescuecore2.standard.entities.PoliceOffice;
import rescuecore2.standard.entities.StandardEntityURN;
import rescuecore2.standard.entities.StandardWorldModel;
import rescuecore2.standard.messages.AKSpeak;
import rescuecore2.standard.messages.AKSubscribe;
import rescuecore2.worldmodel.Entity;
import rescuecore2.worldmodel.EntityID;
import rescuecore2.worldmodel.WorldModel;

/**
 * The channel-based communication model.
 */
public class ChannelCommunicationModel extends AbstractCommunicationModel {
	/** The prefix for channels config options. */
	public static final String PREFIX = "comms.channels.";

	private static final String COUNT_KEY = "comms.channels.count";
	private static final String PLATOON_MAX_CHANNELS_KEY = "comms.channels.max.platoon";
	private static final String CENTRE_MAX_CHANNELS_KEY = "comms.channels.max.centre";

	private static final String TYPE_SUFFIX = ".type";
	private static final String NOISE_SUFFIX = ".noise";
	private static final String INPUT_SUFFIX = ".input";
	private static final String OUTPUT_SUFFIX = ".output";

	private static final String TYPE_VOICE = "voice";
	private static final String TYPE_RADIO = "radio";

	private static final String NOISE_TYPE_DROPOUT = "dropout";
	private static final String NOISE_TYPE_STATIC = "static";

	private Map<Integer, Channel> channels;
	private int platoonMax;
	private int centreMax;
	private StandardWorldModel world;

	/**
	 * Construct a ChannelCommunicationModel.
	 */
	public ChannelCommunicationModel() {
		channels = new HashMap<Integer, Channel>();
	}

	@Override
	public String toString() {
		return "Channel communication model";
	}

	@Override
	public void initialise(Config config, WorldModel<? extends Entity> model) {
		super.initialise(config, model);
		channels.clear();
		world = StandardWorldModel.createStandardWorldModel(model);
		// Read the channel information
		int count = config.getIntValue(COUNT_KEY);
		for (int i = 0; i < count; ++i) {
			String type = config.getValue(PREFIX + i + TYPE_SUFFIX);
			Channel channel = null;
			if (TYPE_VOICE.equals(type)) {
				channel = new VoiceChannel(config, i, world);
			} else if (TYPE_RADIO.equals(type)) {
				channel = new RadioChannel(config, i);
			} else {
				Logger.error("Unrecognised channel type: " + PREFIX + i
						+ TYPE_SUFFIX + " = '" + type + "'");
			}
			if (channel != null) {
				String key = PREFIX + i + NOISE_SUFFIX;
				Noise input = createNoiseObjects(config, key + INPUT_SUFFIX);
				Noise output = createNoiseObjects(config, key + OUTPUT_SUFFIX);
				channel.setInputNoise(input);
				channel.setOutputNoise(output);
				channels.put(i, channel);
				Logger.info("Created channel: " + channel);
			}
		}
		platoonMax = config.getIntValue(PLATOON_MAX_CHANNELS_KEY, 1);
		centreMax = config.getIntValue(CENTRE_MAX_CHANNELS_KEY, 2);
	}

	@Override
	public void process(int time, Collection<? extends Command> agentCommands) {
		Logger.debug("ChannelCommunicationModel processing commands at time "
				+ time + ": " + agentCommands);
		super.process(time, agentCommands);
		// Update all channels
		for (Channel next : channels.values()) {
			next.timestep();
		}
		// Look for subscription commands
		for (Command next : agentCommands) {
			if (next instanceof AKSubscribe) {
				processSubscribe((AKSubscribe) next);
			}
		}
		// Now push all speak commands through the right channels
		for (Command next : agentCommands) {
			if (next instanceof AKSpeak) {
				try {
					AKSpeak speak = (AKSpeak) next;
					int channelNumber = speak.getChannel();
					Channel channel = channels.get(channelNumber);
					Logger.debug("Processing speak: " + speak);
					if (channel == null) {
						throw new InvalidMessageException(
								"Unrecognised channel: " + channelNumber);
					} else {
						channel.push(speak);
					}
				} catch (InvalidMessageException e) {
					Logger.warn("Invalid message: " + next + ": "
							+ e.getMessage());
				}
			}
		}
		// And find out what each agent can hear
		for (Entity agent : world.getEntitiesOfType(
				StandardEntityURN.FIRE_BRIGADE, StandardEntityURN.FIRE_STATION,
				StandardEntityURN.POLICE_FORCE,
				StandardEntityURN.POLICE_OFFICE,
				StandardEntityURN.AMBULANCE_TEAM,
				StandardEntityURN.AMBULANCE_CENTRE, StandardEntityURN.CIVILIAN)) {
			for (Channel channel : channels.values()) {
				addHearing(agent, channel.getMessagesForAgent(agent));
			}
		}
	}

	/**
	 * Get a view of all registered channels.
	 * 
	 * @return All channels.
	 */
	public Collection<Channel> getAllChannels() {
		return Collections.unmodifiableCollection(channels.values());
	}

	private Noise createNoiseObjects(Config config, String key) {
		ChainedNoise result = new ChainedNoise();
		result.addChild(lookForFailure(config, key));
		result.addChild(lookForDropout(config, key));
		result.addChild(lookForStatic(config, key));
		return result;
	}

	private Noise lookForFailure(Config config, String key) {
		if (config.getBooleanValue(key + ".failure.use", false)) {
			return new FailureNoise(config.getFloatValue(key + ".failure.p"),
					config.getRandom());
		}
		return null;
	}

	private Noise lookForDropout(Config config, String key) {
		if (config.getBooleanValue(key + ".dropout.use", false)) {
			return new DropoutNoise(config.getFloatValue(key + ".dropout.p"),
					config.getRandom());
		}
		return null;
	}

	private Noise lookForStatic(Config config, String key) {
		if (config.getBooleanValue(key + ".static.use", false)) {
			return new StaticNoise(config.getFloatValue(key + ".static.p"),
					config.getRandom());
		}
		return null;
	}

	private void processSubscribe(AKSubscribe sub) {
		Logger.debug("Processing subscribe message : " + sub);
		List<Integer> requested = sub.getChannels();
		EntityID id = sub.getAgentID();
		Entity entity = world.getEntity(id);
		if (entity == null) {
			Logger.warn("Couldn't find entity " + id);
			return;
		}
		int max;
		if (entity instanceof FireBrigade || entity instanceof BirdMan
				|| entity instanceof PoliceForce
				|| entity instanceof AmbulanceTeam
				|| entity instanceof Civilian) {
			max = platoonMax;
		} else if (entity instanceof FireStation
				|| entity instanceof PoliceOffice
				|| entity instanceof AmbulanceCentre) {
			max = centreMax;
		} else {
			Logger.warn("I don't know how to handle subscriptions for this entity: "
					+ entity);
			return;
		}
		if (requested.size() > max) {
			Logger.warn("Agent " + id + " tried to subscribe to "
					+ requested.size() + " channels but only " + max
					+ " allowed");
			return;
		}
		// Unsubscribe from all old channels
		for (Channel next : channels.values()) {
			next.removeSubscriber(entity);
		}
		// Subscribe to new channels
		for (int next : requested) {
			Channel channel = channels.get(next);
			if (channel == null) {
				Logger.warn("Agent " + id
						+ " tried to subscribe to non-existant channel " + next);
			} else {
				channel.addSubscriber(entity);
			}
		}
	}
}
